﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Brainee.NeuralNetwork
{
  public class PerceptronTraining : ITrainingMethod
  {
    #region Private fields

    private double _learningRate = 0.1;
    private UInt32 _maxIterations = 1000;

    #endregion
    #region Public properties

    public double LearningRate
    {
      get { return _learningRate; }
      set { _learningRate = Math.Max (0.0, Math.Min (1.0, value)); }
    }

    public double Momentum
    {
      get { return 0; }
      set {  }
    }

    public UInt32 MaxIterations
    {
      get { return _maxIterations; }
      set { _maxIterations = value; }
    }

    public double Error { get; private set; }
    
    public UInt32 Iterations { get; private set; }

    public double MinError { get; set; }

    #endregion

    public bool Train (Network ann, TrainingSet set)
    {
      // check layers count
      if (ann.Count != 1)
        throw new ArgumentException ("The network must have exactly one layer.");

      for (Iterations = 0; Iterations < _maxIterations; Iterations++)
      {
        Error = 0;

        // run learning procedure for all samples
        foreach (TrainingItem item in set)
        {
          Error += TrainItem (ann, item);
        }

        Error = Math.Sqrt (Error);

        if (Progress != null)
        {
          TrainingProgressEventArgs e = new TrainingProgressEventArgs (Iterations, Error);
          Progress (this, e);
          if (e.Cancel)
            break;
        }

        if (Error == 0)
          break;
      }

      return (Error == 0);
    }

    private double TrainItem (Network ann, TrainingItem item)
    {
      double error = 0.0;

      // compute output of network
      double[] observedOutput = ann.Compute (item.Input);

      Layer layer = ann [0];

      // check output of each neuron and update weights
      for (int j = 0; j < layer.OutputCount; j++)
      {
        double delta = item.Output [j] - observedOutput [j];

        if (delta != 0)
        {
          Neuron neuron = layer [j];

          // Update weights
          for (int k = 0; k < neuron.Size; k++)
          {
            neuron [k] += LearningRate * delta * item.Input [k];
          }

          // Update threshold value
          neuron.Threshold += LearningRate * delta;

          error += (delta * delta);
        }
      }

      return error;
    }

    public event TrainingProgressHandler Progress;
  }
}
